@scream
2年前 提问
1个回答

为什么会出现线程安全问题

帅末
2年前

出现线程安全问题的主要原因:

  • 多线程抢占式执行

    导致线程安全问题的第一大因素就是多线程抢占式执行,想象一下,如果是单线程执行,或者是多线程有序执行,那就不会出现混乱的情况了,不出现混乱的情况,自然就不会出现非线程安全的问题了。

  • 多个线程同时操作一个变量

    如果是多线程同时修改不同的变量(每个线程只修改自己的变量),也是不会出现非线程安全的问题了,比如以下代码,线程 1 修改 number1 变量,而线程 2 修改 number2 变量,最终两个线程执行完之后的结果如下:

      public class ThreadSafe {
          // 全局变量
          private static int number = 0;
          // 循环次数(100W)
          private static final int COUNT = 1_000_000;
          // 线程 1 操作的变量 number1
          private static int number1 = 0;
          // 线程 2 操作的变量 number2
          private static int number2 = 0;
    
          public static void main(String[] args) throws InterruptedException {
              // 线程1:执行 100W 次 number+1 操作
              Thread t1 = new Thread(() -> {
                  for (int i = 0; i < COUNT; i++) {
                      number1++;
                  }
              });
              t1.start();
    
              // 线程2:执行 100W 次 number-1 操作
              Thread t2 = new Thread(() -> {
                  for (int i = 0; i < COUNT; i++) {
                      number2--;
                  }
              });
              t2.start();
    
              // 等待线程 1 和线程 2,执行完,打印 number 最终的结果
              t1.join();
              t2.join();
              number = number1 + number2;
              System.out.println("number=number1+number2 最终结果:" + number);
          }

    以上程序的执行结果如下图所示:

    图片

  • 非原子性操作

    原子性操作是指操作不能再被分隔就叫原子性操作。比如人类吸气或者是呼气这个动作,它是一瞬间一次性完成的,你不可能先吸一半(气),停下来玩会手机,再吸一半(气),这种操作就是原子性操作。而非原子性操作是我现在要去睡觉,但睡觉之前要先上床,再拉被子,再躺下、再入睡等一系列的操作综合在一起组成的,这就是非原子性操作。非原子性操作是有可以被分隔和打断的,比如要上床之前,发现时间还在,先刷个剧、刷会手机、再玩会游戏,甚至是再吃点小烧烤等等,所以非原子性操作有很多不确定性,而这些不确定性就会造成线程安全问题问题。像 i++ 和 i– 这种操作就是非原子的,它在 +1 或 -1 之前,先要查询原变量的值,并不是一次性完成的,所以就会导致线程安全问题。

  • 内存不可见

    所谓内存不可见性,就是线程对某个共享变量在线程自己的缓冲中存在副本的时候对主内存中共享变量的值是不可见的,看不见主存中的值。所以就会导致线程安全问题。

  • 指令重排序(编译器优化)

    指令重排序是指 Java 程序为了提高程序的执行速度,所以会对一下操作进行合并和优化的操作。比如说,张三要去图书馆还书,舍友又让张三帮忙借书,那么程序的执行思维是,张三先去图书馆把自己的书还了,再去一趟图书馆帮舍友把书借回来。而指令重排序之后,把两次执行合并了,张三带着自己的书去图书馆把书先还了,再帮舍友把书借出来,整个流程就执行完了,这是正常情况下的指令重排序的好处。但是指令重排序也有“副作用”,而“副作用”是发生在多线程执行中的,还是以张三借书和帮舍友还书为例,如果张三是一件事做完再做另一件事是没有问题的(也就是单线程执行是没有问题的),但如果是多线程执行,就是两件事由多个人混合着做,比如张三在图书馆遇到了自己的多个同学,于是就把任务分派给多个人一起执行,有人借了几本书、有人借了还了几本书、有人再借了几本书、有人再借了还了几本书,执行的很混乱没有明确的目标,到最后悲剧就发生了,这就是在指令重排序带来的线程安全问题。

在 Java 中,解决线程安全问题的手段有 3 种:

  • 使用线程安全的类,如 AtomicInteger 类。

    AtomicInteger 是线程安全的类,使用它可以将 ++ 操作和 – 操作,变成一个原子性操作,这样就能解决非线程安全的问题了。

  • 使用锁 synchronized 或 ReentrantLock 加锁排队执行。

    同步锁synchronized:synchronized 是 JVM 层面实现的自动加锁和自动释放锁的同步锁。

    可重入锁ReentrantLock:ReentrantLock 可重入锁需要程序员自己加锁和释放锁。

  • 使用线程本地变量 ThreadLocal 来处理。

    使用 ThreadLocal 线程本地变量也可以解决线程安全问题,它是给每个线程独自创建了一份属于自己的私有变量,不同的线程操作的是不同的变量,所以也不会存在非线程安全的问题。